﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Forms;
using SemenQuest.Communication;
using Point = System.Drawing.Point;
using System.ComponentModel;
using AgentEventArgs = SemenQuest.AgentController.AgentEventArgs;

namespace SemenQuest.Models
{
    public class Agent : Control
    {
        #region Components
        private IContainer components;
        private System.Timers.Timer TickTimer;
        private System.Timers.Timer LifeTimer;
        #endregion

        #region Variables
        public Guid Id { get; private set; }

        protected Dictionary<Guid, Conversation> Conversations { get; set; }
        private Queue<SemenQuest.Communication.Message> IncomingMessages { get; set; }

        /// <summary>
        /// Current global angle of the agent (degrees)
        /// North is 270, East 0, South 90, West 180
        /// </summary>
        public Double Angle { get; set; }//protected set; }
        /// <summary>
        /// Current velocity of the agent (pixels/s)
        /// </summary>
        public Double Velocity
        {
            get { return _Velocity; }
            protected set
            {
                if (value < 0)
                    _Velocity = 0;
                else if (value > MaxVelocity)
                    _Velocity = MaxVelocity;
                else
                    _Velocity = value;
            }
        }
        private Double _Velocity;

        /// <summary>
        /// Agent target angle (degrees)
        /// North is 0, East 90, South 180, West 270)
        /// </summary>
        public Double TargetAngle
        {
            get { return _TargetAngle; }
            protected set
            {
                _TargetAngle = (value + 360) % 360;
            }
        }
        private Double _TargetAngle;
        /// <summary>
        /// Agent target velocity (pixels/s)
        /// </summary>
        public Double TargetVelocity
        {
            get { return _TargetVelocity; }
            protected set
            {
                if (value < 0)
                    _TargetVelocity = 0;
                else if (value > MaxVelocity)
                {
                    _TargetVelocity = MaxVelocity;
                }
                else
                    _TargetVelocity = value;
            }
        }
        private Double _TargetVelocity;

        /// <summary>
        /// The preferred minimum distance between agents (pixels)
        /// </summary>
        public Int32 MinDistance { get; protected set; }
        /// <summary>
        /// Max speed of the agent (pixels/second)
        /// </summary>
        public Int32 MaxVelocity { get; protected set; }
        /// <summary>
        /// Visibility range of the agent (pixels)
        /// </summary>
        public Int32 Visibility { get; protected set; }
        /// <summary>
        /// Max speed the agent is able to turn (degrees/second)
        /// </summary>
        public Int32 TurnSpeed { get; protected set; }
        /// <summary>
        /// Acceleration speed of the agent (pixels/second^2)
        /// </summary>
        public Int32 Acceleration { get; protected set; }
        /// <summary>
        /// Breaking speed of the agent (pixels/second^2)
        /// </summary>
        public Int32 BreakSpeed { get; protected set; }
        /// <summary>
        /// Lifetime of the agent (milliseconds)
        /// </summary>
        public Int32 LifeTime
        {
            get
            {
                if (!DesignMode)
                    return (Int32)LifeTimer.Interval;
                else
                    return -1;
            }
            protected set
            {
                if (!DesignMode)
                    LifeTimer.Interval = value;
            }
        }

        /// <summary>
        /// Centerpoint of the Agent
        /// </summary>
        public Point CenterPoint { get { return Location + new Size(Width / 2, Height / 2); } }

        public AgentTypes AgentType { get; protected set; }
        public enum AgentTypes
        {
            None,
            Egg,
            Semen
        }
        protected enum KillReasons
        {
            Unknown,
            AgentControllerKill,
            LifeTimeEnded,
            LotteryWon,
        }
        public DateTime StartOfLife { get; private set; }

        protected Double TickTime { get { return TickTimer.Interval; } }
        #endregion

        #region Events & Delegates
        public event AgentController.AgentHandler AgentDied;
        public event AgentController.AgentHandler AgentSpawned;

        protected delegate void NewMessageHandler(Conversation conversation);
        protected event NewMessageHandler NewMessage;
        #endregion

        #region Constructor
        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.LifeTimer = new System.Timers.Timer();
            this.TickTimer = new System.Timers.Timer();
            ((System.ComponentModel.ISupportInitialize)(this.LifeTimer)).BeginInit();
            ((System.ComponentModel.ISupportInitialize)(this.TickTimer)).BeginInit();
            this.SuspendLayout();
            // 
            // LifeTimer
            // 
            this.LifeTimer.AutoReset = false;
            this.LifeTimer.Interval = 5000D;
            this.LifeTimer.SynchronizingObject = this;
            this.LifeTimer.Elapsed += new System.Timers.ElapsedEventHandler(this.LifeTimer_Elapsed);
            // 
            // TickTimer
            // 
            this.TickTimer.Interval = 16.666D;
            this.TickTimer.SynchronizingObject = this;
            ((System.ComponentModel.ISupportInitialize)(this.LifeTimer)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.TickTimer)).EndInit();
            this.ResumeLayout(false);

        }
        protected Agent()
        {
            InitializeComponent();
            TickTimer.Interval = 1000 / Properties.Settings.Default.SemenRefreshRate;

            #region Graphic settings
            this.SetStyle(
                ControlStyles.UserPaint |
                ControlStyles.AllPaintingInWmPaint |
                ControlStyles.OptimizedDoubleBuffer |
                ControlStyles.SupportsTransparentBackColor,
                true);
            this.UpdateStyles();
            this.BackColor = Color.FromArgb(0, 255, 0, 128);
            #endregion

            TickTimer.Elapsed += CheckMessages;

            Id = Guid.NewGuid();
            this.Conversations = new Dictionary<Guid, Conversation>();
            this.IncomingMessages = new Queue<Communication.Message>();
            Angle = 0;
            Velocity = 0;
            AgentType = AgentTypes.None;
            Hide();
        }

        protected void Agent_SettingsUpdate(String SettingName, Object value)
        {
            String AgentTypeName = System.Enum.GetName(typeof(AgentTypes), AgentType);
            if (SettingName.StartsWith("RefreshRate"))
            {
                TickTimer.Interval = (Int32)value != 0 ? 1000 / (Int32)value : TickTimer.Interval;
            }
            else if (SettingName == "EggImpregnatorsNeeded" || SettingName == "SemenColorMale" ||
                SettingName == "SemenColorFemale" || SettingName == "EggColor")
            {
                return;
            }
            else if (SettingName.StartsWith(AgentTypeName))
            {
                String PropertyName = SettingName.Replace(AgentTypeName, String.Empty);
                var Property = this.GetType().GetProperty(PropertyName);
                Property.SetValue(this, value);
            }
        }
        #endregion

        #region Methods
        public virtual void RunAgent(Boolean randomLocation)
        {
            if (randomLocation)
            {
                Int32 max = Math.Max(this.Width, this.Height);
                Point newLocation = RandomPoint();
                while (
                    newLocation.X + max > this.Parent.Width ||
                    newLocation.Y + max > this.Parent.Height)
                {
                    newLocation = RandomPoint();
                }
                Location = newLocation;
            }

            Show();
            StartOfLife = DateTime.Now;
            LifeTimer.Start();
            TickTimer.Start();
            var handler = AgentSpawned;
            if (handler != null)
            {
                handler.Invoke(this, new AgentEventArgs(AgentEventArgs.AdditionalInfos.Spawned));
            }
        }
        public void KillAgent(Boolean controllerKill)
        {
            KillAgent(controllerKill ? KillReasons.AgentControllerKill : KillReasons.Unknown);
        }
        protected virtual void KillAgent(KillReasons killReason)
        {
            if (InvokeRequired)
            {
                BeginInvoke(new Action<KillReasons>(KillAgent), killReason);
                return;
            }

            TickTimer.Stop();
            LifeTimer.Stop();
            var handler = AgentDied;
            if (handler != null)
            {
                AgentEventArgs args;
                switch (killReason)
                {
                    case KillReasons.AgentControllerKill:
                        args = new AgentEventArgs(AgentEventArgs.AdditionalInfos.AgentControllerKill);
                        break;
                    case KillReasons.LifeTimeEnded:
                        args = new AgentEventArgs(AgentEventArgs.AdditionalInfos.LifeTimeEnded);
                        break;
                    case KillReasons.LotteryWon:
                        args = new AgentEventArgs(AgentEventArgs.AdditionalInfos.WonTheLottery);
                        break;
                    default:
                        args = new AgentEventArgs(AgentEventArgs.AdditionalInfos.Unknown);
                        break;
                }
                handler.Invoke(this, args);
            }

            Hide();
            Dispose();
        }
        protected virtual void LifeEnded()
        {
            KillAgent(KillReasons.LifeTimeEnded);
        }

        protected Point RandomPoint()
        {
            Random x = new Random(DateTime.Now.Millisecond);
            if (this.Parent != null)
            {
                Int32 maxX = this.Parent.Size.Width - this.Size.Width;
                Int32 maxY = this.Parent.Size.Height - this.Size.Height;
                if (maxX < 0 || maxY < 0)
                    return Point.Empty;
                return new Point(x.Next(0, this.Parent.Size.Width - this.Size.Width), x.Next(0, this.Parent.Size.Height - this.Size.Height));
            }
            else
            {
                return Point.Empty;
            }
        }
        protected void TickSubscribe(ElapsedEventHandler handler)
        {
            TickTimer.Elapsed += handler;
        }
        protected void TickUnsubscribe(ElapsedEventHandler handler)
        {
            TickTimer.Elapsed -= handler;
        }

        private void LifeTimer_Elapsed(Object sender, System.Timers.ElapsedEventArgs e)
        {
            LifeEnded();
        }
        #endregion

        #region Communication
        public Boolean RecordMessage(Communication.Message message)
        {
            Monitor.TryEnter(IncomingMessages, 10);

            if (Monitor.IsEntered(IncomingMessages))
            {
                IncomingMessages.Enqueue(message);
                Monitor.Exit(IncomingMessages);
                return true;
            }
            return false;
        }
        private void CheckMessages(Object sender, ElapsedEventArgs e)
        {
            List<Communication.Message> messages = new List<Communication.Message>();

            //Get new messages
            try
            {
                Monitor.TryEnter(IncomingMessages, 10);
                if (Monitor.IsEntered(IncomingMessages))
                {
                    while (IncomingMessages.Count > 0)
                    {
                        messages.Add(IncomingMessages.Dequeue());
                    }
                }
            }
            finally
            {
                if (Monitor.IsEntered(IncomingMessages))
                {
                    Monitor.Exit(IncomingMessages);
                }
            }

            //Handle new messages
            while (messages.Count > 0)
            {

                Communication.Message message = messages[0];
                messages.Remove(message);

                Conversation conversation;

                lock (Conversations)
                {
                    if (!Conversations.TryGetValue(message.Sender, out conversation))
                    {
                        conversation = new Conversation(message.Sender);
                        Conversations.Add(message.Sender, conversation);
                    }
                }
                conversation.AddMessage(message);

                NewMessage.BeginInvoke(conversation, EventEndInvoke, null);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        protected void SendMessage(Agent receiver, Communication.Message message)
        {
            lock (Conversations)
            {
                Conversation con;
                if (!Conversations.TryGetValue(receiver.Id, out con))
                {
                    con = new Conversation(receiver.Id);
                    Conversations.Add(receiver.Id, con);
                }
                con.AddMessage(message);
            }

            receiver.RecordMessage(message);
        }

        private static void EventEndInvoke(IAsyncResult iar)
        {
            var ar = (System.Runtime.Remoting.Messaging.AsyncResult)iar;
            var invokedMethod = (NewMessageHandler)ar.AsyncDelegate;
            try { invokedMethod.EndInvoke(iar); }
            catch { }
        }

        #endregion
    }
}